package com.hclc.util.sftp;

import com.hclc.config.util.AssertUtil;
import com.jcraft.jsch.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.model.Model;
import org.apache.tika.Tika;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.stream.Collectors;

/**
 * sftp上传 工具
 *
 * @ClassName SftpUtil
 * @Author: yurj
 * @Mail：1638234804@qq.com
 * @Date: Create in 17:09 2020/12/27
 * @version: 1.0
 */
@Slf4j
@Component
public class SftpUtil {
    /**
     * sftp服务器配置
     */
    private static SftpServerConfig sftpServerConfig;

    /**
     * boolean字符
     */
    private final String CHARSET_TRUE = "true";
    private final String CHARSET_FALSE = "false";

    /**
     * 输出关键字
     */
    private final String OUTPUT_KEYWORD = "echo";

    /**
     * 斜线
     */
    private final String SLASH = "/";

    /**
     * JSCH session
     */
    private static Session session;

    /**
     * 私有的构造方法
     */
    private SftpUtil() {
    }

    /**
     * 私有的工具对象
     */
    private static SftpUtil sftpUtil;

    /**
     * 获取当前工具实例
     *
     * @param
     * @return com.hclc.util.sftp.SftpUtil
     * @author yurj
     * @version 1.0
     * @date 2020/12/27 17:41
     */
    public static synchronized SftpUtil builder() {
        if (Objects.isNull(sftpUtil)) {
            sftpUtil = new SftpUtil();
        }
        if (Objects.isNull(session) || !session.isConnected()) {
            sftpUtil.sftpUploadUtilLogin();
            AssertUtil.isTrue(session.isConnected(), () -> "连接超时,请重试");
        }
        return sftpUtil;
    }

    /**
     * 远程登陆
     *
     * @param
     * @return void
     * @author yurj
     * @version 1.0
     * @date 2020/12/28 10:11
     */
    private void sftpUploadUtilLogin() {
        // 创建jSch对象
        JSch jSch = new JSch();
        // 获取到jSch的session, 根据用户名、主机ip、端口号获取一个Session对象
        try {
            session = jSch.getSession(sftpServerConfig.getUserName(), sftpServerConfig.getIpAddress(), Integer.valueOf(sftpServerConfig.getDefaultPort()));
            // 设置密码
            session.setPassword(sftpServerConfig.getPassWord());
            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");
            // 为Session对象设置properties
            session.setConfig(config);
            // 设置超时
            session.setTimeout(3000);
            // 通过Session建立连接
            session.connect();
        } catch (JSchException e) {
            log.error("远程主机登录失败,IP = {},userName = {}", sftpServerConfig.getIpAddress(), sftpServerConfig.getUserName(), e);
        }
    }

    /**
     * 关闭连接
     *
     * @param
     * @return void
     * @author yurj
     * @version 1.0
     * @date 2020/12/28 10:11
     */
    private void closeSession() {
        // 调用session的关闭连接的方法
        if (Objects.nonNull(session)) {
            // 如果session不为空,调用session的关闭连接的方法
            session.disconnect();
        }
    }

    /**
     * 将输入流转换为string
     *
     * @param in
     * @return java.lang.String
     * @author yurj
     * @version 1.0
     * @date 2020/12/28 10:12
     */
    public static String processInputStreamData(InputStream in) throws IOException {
        String strData = new BufferedReader(new InputStreamReader(in))
                .lines()
                .parallel()
                .collect(Collectors.joining(System.lineSeparator()));
        in.close();
        return strData;
    }

    /**
     * 将连续输入流转换为String
     *
     * @param in
     * @return java.lang.String
     * @author yurj
     * @version 1.0
     * @date 2020/12/30 14:51
     */
    public static String processInputStreamData(ChannelShell channelShell, OutputStream out, InputStream in) throws IOException {
        /**
         * shell管道本身就是交互模式的。要想停止，有两种方式：
         *  一、人为的发送一个exit命令，告诉程序本次交互结束
         *  二、使用字节流中的available方法，来获取数据的总大小，然后循环去读。
         *  为了避免阻塞
         */
        StringBuilder result = new StringBuilder();
        byte[] tmp = new byte[1024];
        while (true) {
            while (in.available() > 0) {
                int i = in.read(tmp, 0, 1024);
                if (i < 0) {
                    break;
                }
                String s = new String(tmp, 0, i);
                if (s.indexOf("--More--") >= 0) {
                    out.write((" ").getBytes());
                    out.flush();
                }
                result.append(s);
            }
            if (channelShell.isClosed()) {
                break;
            }
            try {
                Thread.sleep(500);
            } catch (Exception e) {
                log.error("流解析异常", e);
            }
        }
        out.close();
        in.close();
        channelShell.disconnect();
        return result.toString();
    }

    /**
     * 执行单条linux命令并返回结果
     *
     * @param
     * @return java.lang.String
     * @author yurj
     * @version 1.0
     * @date 2020/12/30 11:59
     */
    public String executeCommandByExec(String command) throws JSchException, IOException {
        // 单条命令(也可执行符合指令)
        ChannelExec channelExec = (ChannelExec) session.openChannel("exec");
        // 执行结果
        InputStream execInputStream = channelExec.getInputStream();
        channelExec.setCommand(command);
        channelExec.setErrStream(System.err);
        channelExec.connect();
        String data = processInputStreamData(execInputStream);
        channelExec.disconnect();
        return data;
    }

    /**
     * 执行多条linux命令并返回结果
     *
     * @param commandList
     * @return java.lang.String
     * @author yurj
     * @version 1.0
     * @date 2020/12/30 14:47
     */
    public String executeCommandByShell(List<String> commandList) throws IOException, JSchException {
        ChannelShell channelShell = (ChannelShell) session.openChannel("shell");
        // 执行结果
        InputStream inputStream = channelShell.getInputStream();
        channelShell.setPty(true);
        channelShell.connect();
        // 待输出命令
        OutputStream outputStream = channelShell.getOutputStream();
        //  使用PrintWriter 就是为了使用println 这个方法
        //  好处就是不需要每次手动给字符加\n
        PrintWriter printWriter = new PrintWriter(outputStream);
        commandList.stream().forEach(s -> {
            printWriter.println(s);
        });
        printWriter.println("exit");//为了结束本次交互
        printWriter.flush();//把缓冲区的数据强行输出
        String result = processInputStreamData(channelShell, outputStream, inputStream);
        return result;
    }

    /**
     * 上传文件
     *
     * @param originalFilename
     * @param file
     * @return java.lang.String
     * @author yurj
     * @version 1.0
     * @date 2021/1/6 16:20
     */
    public String uploadFile(String originalFilename, MultipartFile file) {
        // 根目录pom资源
        Model rootResource = MavenModelResourceUtil.getRootResource();
        // 拼接目录
        String directory = sftpServerConfig.getRootDirectory() + rootResource.getArtifactId();
        return uploadFile(originalFilename, file, directory);
    }

    /**
     * 上传文件
     *
     * @param originalFilename
     * @param file
     * @param directory
     * @return java.lang.String
     * @author yurj
     * @version 1.0
     * @date 2020/12/28 10:12
     */
    public String uploadFile(String originalFilename, MultipartFile file, String directory) {
        String url = StringUtils.EMPTY;
        try {
            // 打开channelSftp
            ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp");
            // 远程连接
            channelSftp.connect();
            // 判断文件夹是否存在
            if (!objectExists(directory, ObjectTypeEnum.OBJECT_DIRECTORY)) {
                // 只能创建一层目录,多层会异常
                //channelSftp.mkdir(directory);
                // 初始化上传文件夹
                initDirectory(directory);
            }
            // 获取文件归属文件夹
            String mimeTypeDirectory = getMimeTypeByStream(file.getInputStream());
            directory = directory + SLASH + mimeTypeDirectory;
            // 进入该目录
            channelSftp.cd(directory);
            // 输出资源到根目录
            channelSftp.put(file.getInputStream(), originalFilename, ChannelSftp.OVERWRITE);
            // 关闭连接
            closeSession();
            // 切断远程连接
            channelSftp.exit();
            url = sftpServerConfig.getHosts() + directory + SLASH + originalFilename;
            log.debug(originalFilename + " 文件上传成功.....");
        } catch (IOException e) {
            log.error("上传文件读取流异常,message = {}", e.getMessage(), e);
        } catch (JSchException e) {
            log.error("JSch异常,message = {}", e.getMessage(), e);
        } catch (SftpException e) {
            log.error("Sftp异常,message = {}", e.getMessage(), e);
        }
        return url;
    }

    /**
     * 初始化上传文件夹
     *
     * @param directory
     * @return void
     * @author yurj
     * @version 1.0
     * @date 2021/1/9 14:31
     */
    private void initDirectory(String directory) {
        String mimeDirectory = MimeTypeEnum.mimeTypeEnums.stream().map(s -> StringUtils.lowerCase(s.getNameValue())).collect(Collectors.joining(","));
        // 命令行
        StringBuilder commandBuilder = new StringBuilder();
        commandBuilder.append("mkdir -p");
        commandBuilder.append(StringUtils.SPACE);
        commandBuilder.append(directory);
        commandBuilder.append(SLASH);
        commandBuilder.append("{");
        commandBuilder.append(mimeDirectory);
        commandBuilder.append("}");
        try {
            String result = executeCommandByExec(commandBuilder.toString());
            log.debug("初始化文件夹,result ={}", result);
        } catch (Exception e) {
            log.error("初始化文件夹远程执行linux单命令出错", e);
        }
    }

    /**
     * 获取当前流文件所属文件夹
     *
     * @param in
     * @return java.lang.String
     * @author yurj
     * @version 1.0
     * @date 2021/1/9 14:56
     */
    private String getMimeTypeByStream(InputStream in) throws IOException {
        // 检测文件mimeType
        Tika tika = new Tika();
        String mimeType = tika.detect(in);
        // 文件所属文件夹类型
        String currentFileType = MimeTypeEnum.mimeTypeEnums.stream()
                .map(s -> StringUtils.lowerCase(s.getNameValue()))
                .filter(s -> mimeType.startsWith(s))
                .findFirst()
                .orElse(StringUtils.lowerCase(MimeTypeEnum.APPLICATION.getNameValue()));
        return currentFileType;
    }

    /**
     * 文件下载
     *
     * @param src linux服务器文件地址
     * @param dst 本地存放地址
     * @return void
     * @author yurj
     * @version 1.0
     * @date 2020/12/28 10:13
     */
    public void fileDownload(String src, String dst) {
        try {
            // src 是linux服务器文件地址,dst 本地存放地址
            ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp");
            // 远程连接
            channelSftp.connect();
            // 下载文件,多个重载方法
            channelSftp.get(src, dst);
            // 切断远程连接,quit()等同于exit(),都是调用disconnect()
            channelSftp.quit();
            // 关闭连接
            closeSession();
            log.debug("src = {} ,下载文件成功.....", src);
        } catch (Exception e) {
            log.error("远程连接失败......", e);
        }
    }

    /**
     * 删除文件
     *
     * @param directoryFile 要删除文件所在目录
     * @return void
     * @author yurj
     * @version 1.0
     * @date 2020/12/28 10:14
     */
    public void deleteFile(String directoryFile) {
        try {
            // 打开openChannel的sftp
            ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp");
            // 远程连接
            channelSftp.connect();
            // 删除文件
            channelSftp.rm(directoryFile);
            // 切断远程连接
            channelSftp.exit();
        } catch (Exception e) {
            log.error("删除文件失败", e);
        }
    }

    /**
     * 判定当前目录下指定类型对象是否存在
     *
     * @param objectName
     * @param type
     * @return boolean
     * @author yurj
     * @version 1.0
     * @date 2020/12/30 16:54
     *
     * <p>
     * linux语法示例
     * [ -f hello.txt ] && echo yes || echo no
     */
    public boolean objectExists(String objectName, ObjectTypeEnum type) {
        // 根据类型拼接命令
        String command = "[ " + type.getCommand() + " " + objectName + " ] && " + OUTPUT_KEYWORD + " " + CHARSET_TRUE + " || " + OUTPUT_KEYWORD + " " + CHARSET_FALSE;
        try {
            String result = executeCommandByExec(command);
            return Objects.equals(CHARSET_TRUE, result);
        } catch (Exception e) {
            log.error("判定对象远程执行linux单命令出错", e);
        }
        return false;
    }

    /**
     * 对象类型枚举
     * <p>
     * command扩展：
     * -e 判断对象是否存在
     * -d 判断对象是否存在，并且为目录
     * -f 判断对象是否存在，并且为常规文件
     * -L 判断对象是否存在，并且为符号链接
     * -h 判断对象是否存在，并且为软链接
     * -s 判断对象是否存在，并且长度不为0
     * -r 判断对象是否存在，并且可读
     * -w 判断对象是否存在，并且可写
     * -x 判断对象是否存在，并且可执行
     * -O 判断对象是否存在，并且属于当前用户
     * -G 判断对象是否存在，并且属于当前用户组
     * -nt 判断file1是否比file2新  [ "/data/file1" -nt "/data/file2" ]
     * -ot 判断file1是否比file2旧  [ "/data/file1" -ot "/data/file2" ]
     */
    @AllArgsConstructor
    public enum ObjectTypeEnum {
        OBJECT_FILE(1, "文件", "-f"),

        OBJECT_DIRECTORY(2, "目录", "-d");

        private final int code;
        private final String desc;
        private final String command;

        public int getCode() {
            return code;
        }

        public String getDesc() {
            return desc;
        }

        public String getCommand() {
            return command;
        }

        public static ObjectTypeEnum find(int code) {
            for (ObjectTypeEnum messageType : ObjectTypeEnum.values()) {
                if (messageType.getCode() == code) {
                    return messageType;
                }
            }
            return null;
        }
    }

    /**
     * 文件扩展类型枚举
     */
    @AllArgsConstructor
    public enum MimeTypeEnum {
        IMAGE(1, "图片"),

        AUDIO(2, "音频"),

        VIDEO(3, "视频"),

        TEXT(4, "文本"),

        APPLICATION(5, "应用文件");

        private final int code;
        private final String desc;

        public int getCode() {
            return code;
        }

        public String getDesc() {
            return desc;
        }

        /**
         * 文件扩展类型set
         */
        static EnumSet<MimeTypeEnum> mimeTypeEnums = EnumSet.allOf(MimeTypeEnum.class);

        public static MimeTypeEnum find(int code) {
            for (MimeTypeEnum messageType : MimeTypeEnum.values()) {
                if (messageType.getCode() == code) {
                    return messageType;
                }
            }
            return null;
        }

        public String getNameValue() {
            return String.valueOf(this);
        }
    }

    /**
     * 初始化sftp配置
     *
     * @param sftpServerConfig
     * @return void
     * @author yurj
     * @version 1.0
     * @date 2021/1/6 16:57
     */
    @Autowired
    public void setSftpServerConfig(SftpServerConfig sftpServerConfig) {
        SftpUtil.sftpServerConfig = sftpServerConfig;
    }
}
